Skip to content

Loosen NUT-29 batch quote checks - #422

Open
Egge21M wants to merge 12 commits into
cashubtc:mainfrom
Egge21M:agent/nut-29-omit-unhandled-quote-checks
Open

Loosen NUT-29 batch quote checks#422
Egge21M wants to merge 12 commits into
cashubtc:mainfrom
Egge21M:agent/nut-29-omit-unhandled-quote-checks

Conversation

@Egge21M

@Egge21M Egge21M commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Implementations

What changed

  • require mints to omit unknown or malformed quote IDs from NUT-29 batch-check responses
  • preserve the relative request order of quotes that are returned
  • define an empty array response when no requested quote IDs can be handled
  • update the NUT-29 test vector for partial and empty results
  • replace stale mint-quote state references with NUT-04's amount_paid, amount_issued, and updated_at accounting model
  • validate batch-mint eligibility using the currently mintable amount, amount_paid - amount_issued

Why

Batch quote checking is a read-only query. A wallet should still receive usable quote states when some requested identifiers are malformed or unknown, without weakening the atomic validation of the batch mint endpoint.

Impact

Mints can process batch-check requests on a best-effort basis. Wallets must treat the response as an order-preserving subset of the requested quotes.

Batch-check responses and batch-mint validation now use NUT-04's canonical mint-quote accounting fields instead of the deprecated state model.

Batch mint execution remains atomic and unchanged.

Validation

  • npx prettier@3.9.1 --check 29.md tests/29-tests.md
  • git diff --check origin/main...HEAD

@github-project-automation github-project-automation Bot moved this to Backlog in nuts Aug 12, 2026
@Egge21M
Egge21M marked this pull request as ready for review August 12, 2026 09:01
@asmogo

asmogo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

I got two questions:

  1. Why are we still using the state in the mint quote batch response? I think this was deprecated in nut-04.

  2. Do you think we should omit unknown quote IDs? Instead of omitting, we could signal to the client that there is an unknown id included. This should make wallet implementations more straightforward.

@Egge21M
Egge21M marked this pull request as draft August 12, 2026 09:34

@robwoodgate robwoodgate left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK the concept. This will only be implemented on mints who will also be aware of the UUIDv7 quote id requirement, so my concerns about creating a batch oracle are avoided (UUIDv7 has 74 bits of entropy per millisecond timestamp).

We should also consider making UUIDv7 a MUST, at least until all quotes are locked.

I still think we should return unknowns, as this tells a wallet unequivocably the quote should be dropped (just omitting unknowns could indicate a mint bug, truncated reponse or other ambiguities).

My suggestion is to include a positional "unknown", as this also makes response processing simpler: quoteids in = quote responses out in same order

Comment thread 29.md Outdated
Comment thread 29.md Outdated
Comment thread tests/29-tests.md
@Egge21M
Egge21M marked this pull request as ready for review August 12, 2026 12:36
Egge21M and others added 6 commits August 12, 2026 14:38
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
The merge dropped the closing bracket on the check response example,
leaving invalid JSON. Also lowercases the entry index, backticks the
field references, and normalises the unknown entry block to 2-space
indent.

@robwoodgate robwoodgate left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies I munged the suggestions last time, so made two commits to fix them up after you merged them. Below are the remaining suggestions to make it consistent.

Comment thread 29.md
Comment thread tests/29-tests.md Outdated
Comment thread tests/29-tests.md Outdated
Comment thread tests/29-tests.md Outdated
]
```

If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array.
If the mint cannot handle any of the requested quote IDs, every entry in the response is an `unknown` entry. The response is never shorter than the request.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In CTS, I have a failIf(quotes.length != responses.length), so if the view is to make an exception for fully failed lookup, I will need to loosen that.

Egge21M and others added 3 commits August 12, 2026 15:43
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Comment thread 29.md
### Unknown or Malformed Quote IDs

This is a query endpoint that uses all-or-nothing error handling, matching the behavior of the batch mint endpoint:
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should name the return type, as we do for other response shapes.

Suggested change
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs.
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `UnknownQuote` for it as below and continue processing the remaining IDs.

@a1denvalu3 a1denvalu3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@a1denvalu3 a1denvalu3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitting my earlier review into threads on the relevant lines. The two I'd block on are the empty-array contradiction in the test vector and the undefined equality check for partially-minted quotes.

Comment thread tests/29-tests.md Outdated
]
```

If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array.

@a1denvalu3 a1denvalu3 Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts 29.md: the spec now says a successful response always has exactly one entry per requested ID, but this line still says an empty array is returned. An empty response to a non-empty request breaks the positional contract. Shouldn't this read that every entry is an unknown entry (response never shorter than the request)?

EDIT: just needs to be updated ig

Comment thread 29.md Outdated
5. **Currency unit consistency**: All quotes MUST use the same currency unit
6. **Quote state**: All quotes MUST be in PAID state (or have a mintable amount for payment methods that allow multiple mint operations like bolt12)
6. **Mintable amount**: All quotes MUST have a positive currently mintable amount (`amount_paid - amount_issued`)
7. **Amount balance**: The sum of amounts contained in the `outputs` MUST equal the sum of `quote_amounts` (bolt11) or MUST NOT exceed it (bolt12)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With rule 6 loosened to "positive mintable amount", a batch can now include an already partially-minted bolt11 quote — but this equality check doesn't say what the target is in that case: the quote's original amount, or its currently mintable amount (amount_paid - amount_issued)? If it's the original amount, a mint can be pushed past NUT-04's cap. Worth defining, including what quote_amounts means for a partially-issued quote.

Comment thread 29.md Outdated
#### Example
Known quotes are returned as mint quote objects as defined by the payment method's NUT specification. A quote ID that the mint does not know or cannot parse is returned as an [unknown entry](#unknown-or-malformed-quote-ids).

Mints **MUST** return the quote object for every quote ID they hold a record of, including quotes whose `expiry` has passed. An unknown entry indicates only that the mint holds no record of the ID.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about an ID the mint does hold, but under a different method (a bolt12 ID sent to /quote/bolt11/check), or a melt quote? Returning the object leaks cross-method existence; returning unknown contradicts "holds no record of the ID". Suggest specifying the endpoint is scoped to {method} and anything outside that scope MUST be returned as unknown.

Comment thread 29.md Outdated
The mint returns a JSON array with exactly one entry per requested quote ID, in request order. Entry `i` corresponds to `quotes[i]`, and its `quote` field **MUST** equal the requested ID.

#### Example
Known quotes are returned as mint quote objects as defined by the payment method's NUT specification. A quote ID that the mint does not know or cannot parse is returned as an [unknown entry](#unknown-or-malformed-quote-ids).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth being explicit about what "mint quote objects as defined by the payment method's NUT" includes here. Returning the full object (incl. request) makes this endpoint an efficient existence oracle: per-entry unknowns leak ~N bits per query vs ~1 bit with all-or-nothing, and NUT-04 only makes UUIDv7 a SHOULD. Either require strong quote IDs for mints advertising NUT-29, or return only the accounting subset (quote, amount_paid, amount_issued, updated_at) — the test vector already shows that minimal shape.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With real-life rate limits and mints already shipping UUIDv7 I dont think that is an issue. We previously discussed this. We could change to UUIDv7 to MUST though just to be explicit

Comment thread 29.md
{
"quote": <str>,
"unknown": true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest stating that unknown entries contain exactly quote and unknown: true, so wallets have a clean discriminator and an entry can't be both a full quote object and unknown.

Comment thread 29.md Outdated
4. **Payment method consistency**: All quotes MUST have the same payment method, matching `{method}` in the URL path
5. **Currency unit consistency**: All quotes MUST use the same currency unit
6. **Quote state**: All quotes MUST be in PAID state (or have a mintable amount for payment methods that allow multiple mint operations like bolt12)
6. **Mintable amount**: All quotes MUST have a positive currently mintable amount (`amount_paid - amount_issued`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the PAID-state rule also drops any mention of expiry from mint validation: nothing in this list says whether an expired (but paid) quote can still be batch-minted. Some mints will reject, some won't — should be stated explicitly, especially since the check section now requires returning expired quotes.

Comment thread 29.md Outdated
@@ -28,13 +28,19 @@ The wallet includes the following body in its request:

where `quotes` is an array of _unique_ mint quote IDs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the check endpoint specifically: is an empty quotes array allowed (→ empty response)? And are duplicate IDs rejected like in batch mint (11016), or answered positionally? Both are undefined for check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

4 participants